﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Web;

namespace System.Web.Mvc
{
	/// <summary>
	/// Removes Required validation on enums marked with [FlagsAttribute]
	/// <para>Automatically encodes the properties which have the HtmlEncodeAttribute(true)</para>
	/// <para>If user has provided no value, the binder automatically sets the value to the provided DefaultValueAttribute for that property</para>
	/// </summary>
	/// <created author="laurentiu.macovei" date="Sat, 11 Feb 2012 00:16:42 GMT"/>
	public class EnumSafeModelBinder
		: DefaultModelBinder
	{
		/// <summary>
		/// Sets the specified property by using the specified controller context, binding
		/// context, and property value.
		/// </summary>
		/// <param name="controllerContext">
		/// The context within which the controller operates. The context information
		/// includes the controller, HTTP content, request context, and route data.
		/// </param>
		/// <param name="bindingContext">
		/// The context within which the model is bound. The context includes information
		/// such as the model object, model name, model type, property filter, and value
		/// provider.
		/// </param>
		/// <param name="propertyDescriptor">
		/// Describes a property to be set. The descriptor provides information such
		/// as the component type, property type, and property value. It also provides
		/// methods to get or set the property value.
		/// </param>
		/// <param name="value">The value to set for the property.</param>
		/// <created author="laurentiu.macovei" date="Mon, 28 Mar 2011 22:39:37 GMT"/>
		protected override void SetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor, object value)
		{
			if (SetPropertyWithDefaultAttribute(controllerContext, bindingContext, propertyDescriptor, value))
				base.SetProperty(controllerContext, bindingContext, propertyDescriptor, value);
		}
		/// <summary>Binds the model by using the specified controller context and binding context.</summary>
		/// <param name="controllerContext">The context within which the controller operates. The context information includes the controller, HTTP content, request context, and route data.</param>
		/// <param name="bindingContext">The context within which the model is bound. The context includes information ///     such as the model object, model name, model type, property filter, and value provider.</param>
		/// <exception cref="System.ArgumentNullException">The bindingContext parameter is null.</exception>
		/// <returns>The bound object.</returns>
		/// <created author="laurentiu.macovei" date="Fri, 08 Apr 2011 17:14:22 GMT"/>
		public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
		{
			var value = base.BindModel(controllerContext, bindingContext);
			EnumSafeModelBinder.EnsureModelIsValid(controllerContext, bindingContext, value);

			return value;
		}

		/// <summary>
		/// </summary>
		/// <param name="controllerContext"></param>
		/// <param name="bindingContext"></param>
		/// <param name="model"></param>
		/// <created author="laurentiu.macovei" date="Fri, 08 Apr 2011 17:14:22 GMT"/>
		public static void EnsureModelIsValid(ControllerContext controllerContext, ModelBindingContext bindingContext, object model)
		{
			foreach (var item in bindingContext.ModelMetadata.Properties)
			{
				var state = bindingContext.ModelState[item.PropertyName] ?? bindingContext.ModelState[bindingContext.ModelName + "." + item.PropertyName];
				if (state == null
					&& !item.IsNullableValueType
					&& item.IsRequired
					&& item.ModelType.IsEnum)
				{
					foreach (var validator in item.GetValidators(controllerContext))
					{
						validator.Validate(model).ForEach(error =>
						{
							if (state == null)
								bindingContext.ModelState[item.PropertyName] = state = new ModelState();
							state.Errors.Add(error.Message);
						});
					}
				}
			}
		}
		/// <summary>Sets the specified property by using the specified controller context, binding context, and property value.</summary>
		/// <param name="controllerContext">The context within which the controller operates. The context information includes the controller, HTTP content, request context, and route data. </param>
		/// <param name="bindingContext">The context within which the model is bound. The context includes information such as the model object, model name, model type, property filter, and value provider. </param>
		/// <param name="propertyDescriptor">Describes a property to be set. The descriptor provides information such as the component type, property type, and property value. It also provides methods to get or set the property value. </param>
		/// <param name="value">The value to set for the property.</param>
		/// <created author="laurentiu.macovei" date="Fri, 08 Apr 2011 17:15:55 GMT"/>
		public static bool SetPropertyWithDefaultAttribute(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor, object value)
		{
			ModelMetadata propertyMetadata = bindingContext.PropertyMetadata[propertyDescriptor.Name];
			string modelStateKey = CreateSubPropertyName(bindingContext.ModelName, propertyMetadata.PropertyName);
			if (value == null)
			{
				//ajax serializer $.fn.serializeObject serializes as array: checkbox_name[] = [true, false]
				if (propertyDescriptor.PropertyType.IsAssignableFrom(typeof(bool)))
				{
					var valueProvider = controllerContext.Controller.ValueProvider.GetValue(modelStateKey + "[]");
					if (valueProvider != null)
						value = valueProvider.ConvertTo(propertyDescriptor.PropertyType);
				}
				else
				{
					//Flags enums are serialized as enum_name[] = [Enum1,Enum3,Enum4]
					if (propertyDescriptor.PropertyType.IsEnum && propertyDescriptor.PropertyType.GetCustomAttributes(typeof(FlagsAttribute), false).Any())
						value = propertyDescriptor.PropertyType.ParseToEnumFlag(controllerContext.HttpContext.Request.Form, modelStateKey + "[]");
				}
			}
			propertyMetadata.Model = value;

			// If the value is null, and the validation system can find a Required validator for
			// us, we'd prefer to run it before we attempt to set the value; otherwise, property
			// setters which throw on null (f.e., Entity Framework properties which are backed by
			// non-nullable strings in the DB) will get their error message in ahead of us.
			//
			// We are effectively using the special validator -- Required -- as a helper to the
			// binding system, which is why this code is here instead of in the Validating/Validated
			// methods, which are really the old-school validation hooks.
			if (value == null && bindingContext.ModelState.IsValidField(modelStateKey))
			{
				ModelValidator requiredValidator = ModelValidatorProviders.Providers.GetValidators(propertyMetadata, controllerContext).Where(v => v.IsRequired).FirstOrDefault();
				if (requiredValidator != null)
				{
					foreach (ModelValidationResult validationResult in requiredValidator.Validate(bindingContext.Model))
					{
						bindingContext.ModelState.AddModelError(modelStateKey, validationResult.Message);
					}
				}
			}

			bool isNullValueOnNonNullableType =
				value == null &&
				!propertyDescriptor.PropertyType.TypeAllowsNullValue();

			if (value is string)
			{
				HtmlEncodeAttribute htmlEncode;
				object htmlEncodeValue;
				if (propertyMetadata.AdditionalValues.TryGetValue("HtmlEncodeAttribute", out htmlEncodeValue) && (htmlEncode = htmlEncodeValue as HtmlEncodeAttribute) != null)
				{
					if (htmlEncode.Encode)
					{
						value = HttpUtility.HtmlEncode(value);
					}
				}

			}
			// Try to set a value into the property unless we know it will fail (read-only
			// properties and null values with non-nullable types)
			object defaultValue;
			bool hasDefaultValue = false;
			if (value == null && propertyMetadata.AdditionalValues.TryGetValue("DefaultValue", out defaultValue))
			{
				value = defaultValue;
				hasDefaultValue = value != null;
				if (hasDefaultValue && value is IConvertible)
					value = ((IConvertible)value).ToType(propertyDescriptor.PropertyType, CultureInfo.CurrentCulture.NumberFormat);
			}

			if (!propertyDescriptor.IsReadOnly && (!isNullValueOnNonNullableType || hasDefaultValue))
			{
				try
				{
					propertyDescriptor.SetValue(bindingContext.Model, value);
				}
				catch (Exception ex)
				{
					// Only add if we're not already invalid
					if (bindingContext.ModelState.IsValidField(modelStateKey))
					{
						bindingContext.ModelState.AddModelError(modelStateKey, ex);
					}
				}
			}
			// Last chance for an error on null values with non-nullable types, we'll use
			// the default "A value is required." message.
			if (isNullValueOnNonNullableType && !hasDefaultValue && bindingContext.ModelState.IsValidField(modelStateKey))
			{
				//enforce the error on the base class

				//bindingContext.ModelState.AddModelError(modelStateKey, GetValueRequiredResource(controllerContext));
				return true;
			}
			return false;
		}

	}
}